package main

import (
	"encoding/json"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"sync"

	"github.com/spf13/cobra"
)

type ModelOptions struct {
	common          CommonOptions
	overrides       []string
	pkgName         string
	dump            bool
	noBase          bool
	template        string
	output          string
	imports         []string
	noDefaultImport bool
}

type ModelDocument struct {
	PkgName string     `json:"pkgName"`
	Imports []string   `json:"imports"`
	Table   *TableDesc `json:"table"`
}

var initModelConfig = sync.OnceFunc(func() {
	initCommonConfig()
})

func getModelTemplate(opts *ModelOptions) string {
	if opts.template != "" {
		return opts.template
	}
	var templateName string
	if getAppConfig().Model != nil {
		templateName = getAppConfig().Model.TemplateName
	}
	if templateName == "" {
		templateName = "default_model"
	}
	return templateName
}

func getTableModelDoc(database string, tableName string, opts *ModelOptions) (*ModelDocument, error) {
	tableDesc, err := getTableDesc(database, tableName, !opts.noBase)
	if err != nil {
		return nil, err
	}
	imports := make(map[string]bool)
	for _, importPath := range opts.imports {
		imports[importPath] = true
	}
	if !opts.noDefaultImport {
		if tableDesc.BaseType != nil {
			imports[tableDesc.BaseType.PkgPath] = true
		}
		for _, field := range tableDesc.Fields {
			var goType *GoType
			if field.SoftDelete {
				goType = softDeleteType
			} else {
				goType = field.GoType
			}
			if goType.PkgPath != "" {
				imports[goType.PkgPath] = true
			}
		}
	}
	delete(imports, "")
	var importList []string
	for k := range imports {
		importList = append(importList, k)
	}
	slices.Sort(importList)

	return &ModelDocument{
		PkgName: opts.pkgName,
		Imports: importList,
		Table:   tableDesc,
	}, nil
}

func generateModel(database string, tableName string, opts *ModelOptions) error {
	doc, err := getTableModelDoc(database, tableName, opts)
	if err != nil {
		return err
	}
	if opts.dump {
		enc := json.NewEncoder(os.Stdout)
		enc.SetEscapeHTML(false)
		enc.SetIndent("", "  ")
		return enc.Encode(doc)
	}

	outputFile := opts.output
	if outputFile == "" {
		outputFile = tableName + ".go"
	}
	return writeOutput(outputFile, filepath.ToSlash(getModelTemplate(opts)), doc, &opts.common)
}

func applyTypeOverrides(overrides []string) {
	for _, over := range overrides {
		parts := strings.SplitN(over, "=", 2)
		if len(parts) == 1 {
			fatalf("invalid override expression: %s\n", over)
		}
		mysqlTypeMap[parts[0]] = parts[1]
	}
}

func modelCommand() *cobra.Command {
	opts := &ModelOptions{}
	cmd := &cobra.Command{
		Use:     "model [flags] <table>...",
		Short:   "Generate model type code",
		Args:    cobra.MinimumNArgs(1),
		Aliases: []string{"m", "schema"},
		PreRun: func(cmd *cobra.Command, args []string) {
			initModelConfig()

			if opts.pkgName == "" {
				wd, _ := os.Getwd()
				wd, _ = filepath.Abs(wd)
				opts.pkgName = filepath.Base(wd)
			}
			if !isValidPkgName(opts.pkgName) {
				fatalf("invalid package name: %s\n", opts.pkgName)
			}
			applyTypeOverrides(opts.overrides)

			err := tryLoadTemplateFile(getModelTemplate(opts))
			if err != nil {
				fatalf("read template file: %v\n", err)
			}
		},
		Run: func(cmd *cobra.Command, args []string) {
			for _, table := range args {
				database, table, _ := splitDatabaseTable(table)
				err := generateModel(database, table, opts)
				if err != nil {
					fatalf("generate model: %v\n", err)
				}
			}
		},
		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
			database, table, prefix := splitDatabaseTable(toComplete)
			tbls, _ := showTables(database, table)
			for i, tbl := range tbls {
				tbls[i] = prefix + tbl
			}
			return tbls, cobra.ShellCompDirectiveNoFileComp
		},
	}
	opts.common.AddFlags(cmd)
	cmd.Flags().StringVarP(&opts.pkgName, "package", "p", "", "specify package name")
	cmd.Flags().BoolVarP(&opts.noBase, "no-base", "B", false, "no base model type")
	cmd.Flags().StringArrayVarP(&opts.overrides, "override", "O", nil, "override pre-defined type: type=type")
	cmd.Flags().BoolVar(&opts.dump, "dump", false, "dump document")
	cmd.Flags().StringVarP(&opts.template, "template", "t", "", "specify template name")
	cmd.Flags().StringVarP(&opts.output, "output", "o", "", "specify output name")
	cmd.Flags().StringArrayVarP(&opts.imports, "import", "I", nil, "add import")
	cmd.Flags().BoolVar(&opts.noDefaultImport, "no-default-import", false, "no default import")
	return cmd
}
